vue-router@3.x实现分析
本文最后更新于:2022年5月31日 早上
vue-router
模式
hash
模式
原理:监听onHashChange
和修改URL
的hash
值#xxx
- 不刷新页面
-
hash
不会发送到服务器 -
hash
的改变会保存历史记录 - 通过
<a>
标签的href
或者location.hash
修改
history
模式
原理:监听history.popState
,通过pushState``replaceState
修改历史栈
- 不刷新页面
- H5
history API
- 服务器需要配合设置地址不匹配回退
-
SEO
比 hash 好
abstract
模式
参数
- params
const routes = [
// 动态字段以冒号开始
{ path: "/users/:id", component: User },
];
- query
URL 地址的 query 部分
/users?id=123
路由钩子
// 全局
router.beforeEach((to,form,next)) // 导航触发时
router.beforeResolve((to,form,next)) // 所有组件内守卫和异步路由组件被解析之后
router.afterEach((to, from, failure))
// 全局钩子可以定义多个,顺序执行完后才会resolve路由
// 路由
beforeEnter() // 不会在 params、query 或 hash 改变触发,只会在导航路径改变触发
{
path:'/a',
component:()=>import('@/component/A'),
beforeEnter:((to,from,next))=>{} //
}
// 组件内
beforeRouteEnter((to,from,next:(vm)=>{/*访问this*/})) // 组件实例创建之前
beforeRouteUpdate((to,from))
/* CompositionAPI onBeforeRouteUpdate */
beforeRouteLeave((to,from))
/* CompositionAPI onBeforeRouteLeave */
触发流程:pageA=>pageB
pageA.
beforeRouteLeave
router.
beforeEach
routes[pageB].
beforeEnter
pageB.
beforeRouteEnter
如果 pageB 渲染过,忽略【3,4】执行 pageB.beforeRouteUpdate
router.
beforeResolve
router.
afterEach
DOM 更新/挂载
执行第【4】步中
next
回调
初始化阶段
Vue.use 安装路由
export function install(Vue) {
if (install.installed && _Vue === Vue) return;
install.installed = true;
_Vue = Vue;
const isDef = (v) => v !== undefined;
const registerInstance = (vm, callVal) => {
let i = vm.$options._parentVnode;
// 伪代码
registerRouteInstance(vm, callVal);
// router-view render函数的registerRouteInstance方法
};
Vue.mixin({
beforeCreate() {
if (isDef(this.$options.router)) {
this._routerRoot = this; // 路由的root组件
this._router = this.$options.router; // 路由实例
this._router.init(this); // init见下文
Vue.util.defineReactive(this, "_route", this._router.history.current);
// 🔥对router-view的响应式绑定
} else {
this._routerRoot = (this.$parent && this.$parent._routerRoot) || this;
// 如果是子组件,将追溯到拥有router的父组件作为root,执行上面几行逻辑
}
registerInstance(this, this);
},
destroyed() {
registerInstance(this);
},
});
// 为Vue的原型绑定`$router``$route`的getter和全局注册两个组件
Object.defineProperty(Vue.prototype, "$router", {
get() {
return this._routerRoot._router;
},
});
Object.defineProperty(Vue.prototype, "$route", {
get() {
return this._routerRoot._route;
},
});
Vue.component("RouterView", View);
Vue.component("RouterLink", Link);
const strats = Vue.config.optionMergeStrategies;
strats.beforeRouteEnter = strats.beforeRouteLeave = strats.beforeRouteUpdate = strats.created;
}
实例化一个 router
class VueRouter {
constructor(options: RouterOptions = {}) {
this.app = null; // 根组件
this.apps = []; // options中有router项的实例
this.options = options; // 路由配置
this.beforeHooks = [];
this.resolveHooks = []; // 钩子
this.afterHooks = [];
this.matcher = createMatcher(options.routes || [], this);
let mode = options.mode || "hash";
this.fallback = mode === "history" && !supportsPushState && options.fallback !== false;
if (this.fallback) {
// 回退到hash模式
mode = "hash";
}
if (!inBrowser) {
mode = "abstract";
}
this.mode = mode;
switch (mode) {
case "history":
this.history = new HTML5History(this, options.base);
break;
case "hash":
this.history = new HashHistory(this, options.base, this.fallback);
break;
case "abstract":
this.history = new AbstractHistory(this, options.base);
break;
}
}
init(app: any) {
this.apps.push(app);
if (this.app) {
return;
}
this.app = app;
const history = this.history;
if (history instanceof HTML5History) {
history.transitionTo(history.getCurrentLocation());
} else if (history instanceof HashHistory) {
const setupHashListener = () => {
history.setupListeners();
};
history.transitionTo(history.getCurrentLocation(), setupHashListener, setupHashListener);
}
history.listen((route) => {
this.apps.forEach((app) => {
app._route = route;
});
});
}
}
路径匹配
createMatcher
export function createMatcher(routes: Array<RouteConfig>, router: VueRouter): Matcher {
// 根据routes构建出path的集合pathList,
// path 到 RouteRecord的映射表pathMap
// name 到 RouteRecord的映射表nameMap
// const record: RouteRecord = {
// path: normalizedPath,
// regex: compileRouteRegex(normalizedPath, pathToRegexpOptions),
// components: route.components || { default: route.component },
// instances: {},
// name,
// parent,
// matchAs,
// redirect: route.redirect,
// beforeEnter: route.beforeEnter,
// meta: route.meta || {},
// props: route.props == null
// ? {}
// : route.components
// ? route.props
// : { default: route.props }
// }
const { pathList, pathMap, nameMap } = createRouteMap(routes);
// 动态添加路由的方法
function addRoutes(routes) {
createRouteMap(routes, pathList, pathMap, nameMap);
}
//
function match(raw: RawLocation, currentRoute?: Route, redirectedFrom?: Location): Route {
const location = normalizeLocation(raw, currentRoute, false, router);
const { name } = location;
if (name) {
const record = nameMap[name];
if (!record) return _createRoute(null, location);
const paramNames = record.regex.keys.filter((key) => !key.optional).map((key) => key.name);
if (typeof location.params !== "object") {
location.params = {};
}
if (currentRoute && typeof currentRoute.params === "object") {
for (const key in currentRoute.params) {
if (!(key in location.params) && paramNames.indexOf(key) > -1) {
location.params[key] = currentRoute.params[key];
}
}
}
if (record) {
location.path = fillParams(record.path, location.params, `named route "${name}"`);
return _createRoute(record, location, redirectedFrom);
}
} else if (location.path) {
location.params = {};
for (let i = 0; i < pathList.length; i++) {
const path = pathList[i];
const record = pathMap[path];
if (matchRoute(record.regex, location.path, location.params)) {
return _createRoute(record, location, redirectedFrom);
}
}
}
return _createRoute(null, location);
}
// ...
function _createRoute(record: ?RouteRecord, location: Location, redirectedFrom?: Location): Route {
// 重定向
if (record && record.redirect) {
return redirect(record, redirectedFrom || location);
}
// alias
if (record && record.matchAs) {
return alias(record, location, record.matchAs);
}
return createRoute(record, location, redirectedFrom, router);
}
//
function createRoute(
record: ?RouteRecord,
location: Location,
redirectedFrom?: ?Location,
router?: VueRouter
): Route {
const stringifyQuery = router && router.options.stringifyQuery;
let query: any = location.query || {};
try {
query = clone(query);
} catch (e) {}
const route: Route = {
name: location.name || (record && record.name),
meta: (record && record.meta) || {},
path: location.path || "/",
hash: location.hash || "",
query,
params: location.params || {},
fullPath: getFullPath(location, stringifyQuery),
matched: record ? formatMatch(record) : [],
};
if (redirectedFrom) {
route.redirectedFrom = getFullPath(redirectedFrom, stringifyQuery);
}
return Object.freeze(route);
}
return {
match,
addRoutes,
};
}
路径切换
transitionTo (location: RawLocation, onComplete?: Function, onAbort?: Function) {
const route = this.router.match(location, this.current)
// 根据目标 location 和当前路径 this.current 执行 this.router.match 方法去匹配到目标的路径
this.confirmTransition(route, () => {
this.updateRoute(route)
onComplete && onComplete(route)
if (!this.ready) {
this.ready = true
this.readyCbs.forEach(cb => { cb(route) })
}
}
//...
)
}
updateRoute (route: Route) {
const prev = this.current
this.current = route
this.cb && this.cb(route)
this.router.afterHooks.forEach(hook => { /* router.afterEach */
hook && hook(route, prev)
})
}
confirmTransition
confirmTransition (route: Route, onComplete: Function, onAbort?: Function) {
const current = this.current
const {
updated,
deactivated,
activated
} = resolveQueue(this.current.matched, route.matched)
// matched数组属性保存了从匹配到的record循环向上一直到最外层的所有的record
// resolve解析出三个队列
// a/b/c => a/b/d
// deactivated : [c]
// activate : [d]
// updated : [a,b]
// 生成一个按照路由钩子顺序的队列
const queue = [].concat(
extractLeaveGuards(deactivated), /* 执行离开的钩子 */
this.router.beforeHooks, /* router.beforeEach */
extractUpdateHooks(updated), /* beforeRouteUpdate */
activated.map(m => m.beforeEnter), /* beforeRouteEnter */
resolveAsyncComponents(activated) /*解析异步路由组件*/
)
runQueue(queue, iterator, () => {
const postEnterCbs = []
const isValid = () => this.current === route
// beforeRouteEnter
const enterGuards = extractEnterGuards(activated, postEnterCbs, isValid)
// ⭐️beforeRouteEnter的回调会添加到postEnterCbs数组中,等路由更新后在最后执行
// router.beforeResolve
const queue = enterGuards.concat(this.router.resolveHooks)
runQueue(queue, iterator, () => {
onComplete(route)
if (this.router.app) {
this.router.app.$nextTick(() => {
postEnterCbs.forEach(cb => { cb() })
// ⭐️执行postEnterCbs中的回调
})
}
})
})
}
<router-view>
把根 Vue
实例的 _route
属性是响应式的,在每个 <router-view>
的render
函数的时候,都会访问 parent.$route
,该<router-view>
就订阅了router
的变化
export default {
name: "RouterView",
functional: true,
props: {
name: {
type: String,
default: "default",
},
},
render(_, { props, children, parent, data }) {
data.routerView = true;
const h = parent.$createElement;
const name = props.name;
const route = parent.$route; // 🔥执行时会触发响应式收集为router的依赖,当router更新时重新执行
const cache = parent._routerViewCache || (parent._routerViewCache = {});
while (parent && parent._routerRoot !== parent) {
parent = parent.$parent;
}
const component = (cache[name] = matched.components[name]);
// 给 matched.instances[name] 赋值当前组件的 vm 实例
data.registerRouteInstance = (vm, val) => {
const current = matched.instances[name];
if ((val && current !== vm) || (!val && current === vm)) {
matched.instances[name] = val;
}
};
(data.hook || (data.hook = {})).prepatch = (_, vnode) => {
matched.instances[name] = vnode.componentInstance;
};
return h(component, data, children);
},
};
归纳
- 在每个组件的
beforeCreate
和destroyed
钩子中混入路由方法 - 依据
routes
中的路由配置,递归生成path/name
映射到组件的映射表RouteRecords
- 对实例的
_route
属性绑定响应式 - 根据选择的
mode
不同,生成不同的history
模式,与地址栏绑定修改 <router-view>
组件的渲染函数会访问_route
属性,订阅了路由状态的改变- 地址栏或者
router-link
改变后修改了_route
matcher
计算出从当前url到目标url的改变路径(matched record
数组)- 对比新旧的
matched
解析出activated``updated``deactivated
三个队列 - 对这三个队列中的组件分别执行相应的执行离开钩子,更新钩子和进入钩子,全局的钩子也会在相应的位置执行
- 通知
router-view
组件重新渲染,router-view
会计算自己组件在vue组件树中位置,去matched
中找到该渲染的组件 - 最后更新路由实例状态
vue-router@3.x实现分析
http://yoursite.com/2022/03/10/[源码]vue2-router/